# -*- coding: UTF-8 -*-
"""
Le système de modèles du CMS; ce système est partagé par tous les modules du CMS
qui exercent un contrôle sur la sortie envoyée au navigateur.

Comment ça marche?

La fonction tokenize() convertit un modèle (c'est-à-dire une chaîne mêlant
balises HTML et balises Django) en unités lexicales; il y a trois catégories
d'unités lexicales : l'une représente du texte brut (TOKEN_TEXT), une autre
représente des variables (TOKEN_VAR) et la dernière représente des blocs
d'instructions (TOKEN_BLOCK).

La classe Parser() accepte comme argument de son constructeur une liste d'unités
lexicales et la méthode parse() de cette classe renvoie un modèle compilé (en
coulisses, un modèle compilé constitue une liste d'objets Node).

Chaque objet Node doit générer une sortie : cela peut être du texte simple (pour
l'objet TextNode), par exemple, ou la valeur d'une variable dans un
contexte particulier (pour l'objet VariableNode), ou encore un résultat logique
(pour l'objet IfNode) ou le résultat d'une boucle (ForNode) ou n'importe quoi
d'autre. Les objets Node de base sont les objets TextNode, VariableNode, IfNode
et ForNode mais chaque module est à même de définir ses propres objets Node.

Chaque objet Node possède une méthode render(), qui accepte en argument un objet
Context et renvoie une chaîne contenant la sortie générée par l'objet Node. À
titre d'exemple, la méthode render() d'un objet VariableNode renvoie la valeur
de la variable correspondante sous forme de chaîne. La méthode render() d'un
objet IfNode renvoie la sortie générée par tous les objets Node contenus à
l'intérieur de l'instruction, et ce de manière récursive.

La classe Template est une classe pratique construite autour de plusieurs autres
et qui facilite la compilation et le rendu de modèles.

Usage :

Normalement, le seul élément de ce module que vous aurez besoin d'utiliser
directement est la classe Template. Créez un objet Template en passant au
constructeur une chaîne contenant le modèle à compiler, puis appelez la méthode
render() en spécifiant un contexte. Pendant la phase de compilation, l'exception
TemplateSyntaxError sera déclenchée si le modèle spécifié n'a pas une syntaxe
correcte.

Exemple de code :

>>> import template
>>> s = '''
... <html>
... {% if test %}
...     <h1>{{ valeur }}</h1>
... {% endif %}
... </html>
... '''
>>> t = template.Template(s)

(t est maintenant un modèle compilé et sa méthode render() peut être appelée
plusieurs fois en spécifiant à chaque fois un contexte différent)

>>> c = template.Context({'test':True, 'valeur': 'Salut'})
>>> t.render(c)
'\n<html>\n\n    <h1>Salut</h1>\n\n</html>\n'
>>> c = template.Context({'test':False, 'valeur': 'Salut'})
>>> t.render(c)
'\n<html>\n\n</html>\n'
"""
import re

__all__ = ('Template','Context','compile_string')

TOKEN_TEXT = 0
TOKEN_VAR = 1
TOKEN_BLOCK = 2

# constantes en lien avec la syntaxe des modèles
FILTER_SEPARATOR = '|'
FILTER_ARGUMENT_SEPARATOR = ':'
VARIABLE_ATTRIBUTE_SEPARATOR = '.'
BLOCK_TAG_START = '{%'
BLOCK_TAG_END = '%}'
VARIABLE_TAG_START = '{{'
VARIABLE_TAG_END = '}}'

ALLOWED_VARIABLE_CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_.'

# expression régulière qui correspond à une balise-variable ou une balise-bloc et qui capture
# cette balise dans sa totalité, y compris les délimiteurs de début et de fin
tag_re = re.compile('(%s.*?%s|%s.*?%s)' % (re.escape(BLOCK_TAG_START), re.escape(BLOCK_TAG_END),
                                          re.escape(VARIABLE_TAG_START), re.escape(VARIABLE_TAG_END)))

# dictionnaire global utilisé par register_tag; associe balises personnalisées aux fonctions correspondantes
registered_tags = {}

# dictionnaire global utilisé par register_filter; associe filtres personnalisés aux fonctions correspondantes
registered_filters = {}

class TemplateSyntaxError(Exception):
    pass

class ContextPopException(Exception):
    "La méthode pop() a été appelée plus de fois que push()"
    pass

class TemplateDoesNotExist(Exception):
    pass

class VariableDoesNotExist(Exception):
    pass

class SilentVariableFailure(Exception):
    "Toute fonction déclenchant cette exception sera ignorée par resolve_variable"
    pass

class Template:
    def __init__(self, template_string):
        "Phase de compilation"
        self.nodelist = compile_string(template_string)

    def __iter__(self):
        for node in self.nodelist:
            for subnode in node:
                yield subnode

    def render(self, context):
        "Phase d'affichage (peut être effectuée plusieurs fois)"
        return self.nodelist.render(context)

def compile_string(template_string):
    "Compile la chaîne spécifiée (qui doit être un modèle brut) en une NodeList prête à l'affichage"
    tokens = tokenize(template_string)
    parser = Parser(tokens)
    return parser.parse()

class Context:
    "Un conteneur de type pile qui sert à représenter un contexte"
    def __init__(self, dict={}):
        self.dicts = [dict]

    def __repr__(self):
        return repr(self.dicts)

    def __iter__(self):
        for d in self.dicts:
            yield d

    def push(self):
        self.dicts = [{}] + self.dicts

    def pop(self):
        if len(self.dicts) == 1:
            raise ContextPopException
        del self.dicts[0]

    def __setitem__(self, key, value):
        "Définit une variable dans le contexte actuel"
        self.dicts[0][key] = value

    def __getitem__(self, key):
        "Obtient la valeur d'une variable, en commençant par le contexte actuel et en allant vers le haut de la pile"
        for dict in self.dicts:
            if dict.has_key(key):
                return dict[key]
        return ''

    def __delitem__(self, key):
        "Supprime une variable du contexte actuel"
        del self.dicts[0][key]

    def has_key(self, key):
        for dict in self.dicts:
            if dict.has_key(key):
                return True
        return False

    def update(self, other_dict):
        "Dans la veine de la méthode dict.update(), cette méthode ajoute au bas de la pile un dictionnaire complet avec clés et valeurs"
        self.dicts = [other_dict] + self.dicts

class Token:
    def __init__(self, token_type, contents):
        "L'argument token_type doit être TOKEN_TEXT, TOKEN_VAR ou TOKEN_BLOCK"
        self.token_type, self.contents = token_type, contents

    def __str__(self):
        return '<unité lexicale de type %s : "%s...">' % (
            {TOKEN_TEXT:'Texte', TOKEN_VAR:'Variable', TOKEN_BLOCK:'Bloc'}[self.token_type],
            self.contents[:20].replace('\n', '')
            )

def tokenize(template_string):
    "Renvoie une liste d'unités lexicales à partir de la chaîne spécifiée (qui doit être un modèle)"
    # on supprime toutes les chaînes vides car l'expression régulière utilisée a une légère tendance à en ajouter
    bits = filter(None, tag_re.split(template_string))
    return map(create_token, bits)

def create_token(token_string):
    "Convertit la chaîne spécifiée en un nouvel objet Token et le renvoie"
    if token_string.startswith(VARIABLE_TAG_START):
        return Token(TOKEN_VAR, token_string[len(VARIABLE_TAG_START):-len(VARIABLE_TAG_END)].strip())
    elif token_string.startswith(BLOCK_TAG_START):
        return Token(TOKEN_BLOCK, token_string[len(BLOCK_TAG_START):-len(BLOCK_TAG_END)].strip())
    else:
        return Token(TOKEN_TEXT, token_string)

class Parser:
    def __init__(self, tokens):
        self.tokens = tokens

    def parse(self, parse_until=[]):
        nodelist = NodeList()
        while self.tokens:
            token = self.next_token()
            if token.token_type == TOKEN_TEXT:
                nodelist.append(TextNode(token.contents))
            elif token.token_type == TOKEN_VAR:
                if not token.contents:
                    raise TemplateSyntaxError, "Balise-variable vide"
                nodelist.append(VariableNode(token.contents))
            elif token.token_type == TOKEN_BLOCK:
                if token.contents in parse_until:
                    # on replace l'unité lexicale dans la liste afin que le code réalisant l'appel
                    # sache pourquoi on s'est arrêté
                    self.prepend_token(token)
                    return nodelist
                try:
                    command = token.contents.split()[0]
                except IndexError:
                    raise TemplateSyntaxError, "Balise-bloc vide"
                try:
                    # on exécute la fonction correspondant à la balise et on ajoute à la liste
                    # l'objet Node qui en résulte
                    nodelist.append(registered_tags[command](self, token))
                except KeyError:
                    raise TemplateSyntaxError, "Balise-bloc invalide : '%s'" % command
        if parse_until:
            raise TemplateSyntaxError, "Balise(s) non-fermée(s) : '%s'" % ', '.join(parse_until)
        return nodelist

    def next_token(self):
        return self.tokens.pop(0)

    def prepend_token(self, token):
        self.tokens.insert(0, token)

    def delete_first_token(self):
        del self.tokens[0]

class FilterParser:
    """Analyse syntaxiquement une unité lexicale Variable et ses filtres facultatifs (l'unité lexicale
       doit être passée sous forme de chaîne) et renvoie une liste de tuples contenant
       le nom du filtre et son ou ses arguments.

       Exemple :
            >>> unite = 'variable|default:"Valeur par défaut"|date:"d-m-Y"'
            >>> p = FilterParser(unite)
            >>> p.filters
            [('default', 'Valeur par défaut'), ('date', 'd-m-Y')]
            >>> p.var
            'variable'

        Normalement, cette classe ne sera jamais instanciée en
        dehors de la fonction get_filters_from_token.
    """
    def __init__(self, s):
        self.s = s
        self.i = -1
        self.current = ''
        self.filters = []
        self.current_filter_name = None
        self.current_filter_arg = None
        # D'abord, on lit la partie correspondant au nom de la variable
        self.var = self.read_alphanumeric_token()
        if not self.var:
            raise TemplateSyntaxError, "Impossible de lire un nom de variable : '%s'" % self.s
        if self.var.find(VARIABLE_ATTRIBUTE_SEPARATOR + '_') > -1 or self.var[0] == '_':
            raise TemplateSyntaxError, "Les noms de variable et d'attribut ne peuvent pas commencer par un tiret bas : '%s'" % self.var
        # Est-ce qu'on a atteint la fin?
        if self.current is None:
            return
        if self.current != FILTER_SEPARATOR:
            raise TemplateSyntaxError, "Caractère '%s' incorrect (s'attendait à '%s')" % (self.current, FILTER_SEPARATOR)
        # Il y a bien un séparateur annonçant un filtre; on commence donc à lire les filtres
        self.read_filters()

    def next_char(self):
        self.i = self.i + 1
        try:
            self.current = self.s[self.i]
        except IndexError:
            self.current = None

    def read_alphanumeric_token(self):
        """Lit le nom d'une variable ou le nom d'un filtre; les noms de variable ou de filtre sont des chaînes
        constituées de caractères alphanumériques et de tirets bas."""
        var = ''
        while 1:
            self.next_char()
            if self.current is None:
                break
            if self.current not in ALLOWED_VARIABLE_CHARS:
                break
            var += self.current
        return var

    def read_filters(self):
        while 1:
            filter_name, arg = self.read_filter()
            if not registered_filters.has_key(filter_name):
                raise TemplateSyntaxError, "Filtre invalide : '%s'" % filter_name
            if registered_filters[filter_name][1] == True and arg is None:
                raise TemplateSyntaxError, "Le filtre '%s' nécessite un argument" % filter_name
            if registered_filters[filter_name][1] == False and arg is not None:
                raise TemplateSyntaxError, "Le filtre '%s' ne devrait pas avoir d'argument" % filter_name
            self.filters.append((filter_name, arg))
            if self.current is None:
                break

    def read_filter(self):
        self.current_filter_name = self.read_alphanumeric_token()
        # Est-ce qu'on a atteint la fin?
        if self.current is None:
            return (self.current_filter_name, None)
        # Est-ce que le filtre possède un argument?
        if self.current == FILTER_ARGUMENT_SEPARATOR:
            self.current_filter_arg = self.read_arg()
            return (self.current_filter_name, self.current_filter_arg)
        # Le prochain caractère doit être un |
        if self.current != FILTER_SEPARATOR:
            raise TemplateSyntaxError, "Caractère '%s' incorrect (s'attendait à '%s')" % (self.current, FILTER_SEPARATOR)
        return (self.current_filter_name, self.current_filter_arg)

    def read_arg(self):
        # D'abord on lit un "
        self.next_char()
        if self.current != '"':
            raise TemplateSyntaxError, "Caractère '%s' incorrect (s'attendait à \")" % self.current
        self.escaped = False
        arg = ''
        while 1:
            self.next_char()
            if self.current == '"' and not self.escaped:
                break
            if self.current == '\\' and not self.escaped:
                self.escaped = True
                continue
            if self.current == '\\' and self.escaped:
                arg += '\\'
                self.escaped = False
                continue
            if self.current == '"' and self.escaped:
                arg += '"'
                self.escaped = False
                continue
            if self.escaped and self.current not in '\\"':
                raise TemplateSyntaxError, "Antislash non-échappé dans '%s'" % self.s
            if self.current is None:
                raise TemplateSyntaxError, "Fin inattendue de l'argument dans '%s'" % self.s
            arg += self.current
        # self.current doit maintenant être '"'
        self.next_char()
        return arg

def get_filters_from_token(token):
    "Fonction pratique construite autour de FilterParser"
    p = FilterParser(token)
    return (p.var, p.filters)

def resolve_variable(path, context):
    """
    Renvoie la valeur de la variable ou de l'attribut spécifié, dans le contexte
    spécifié.

    >>> c = {'article': {'section':'Infos'}}
    >>> resolve_variable('article.section', c)
    'Infos'
    >>> resolve_variable('article', c)
    {'section': 'Infos'}
    >>> class UneClasse: pass
    >>> c = UneClasse()
    >>> c.article = UneClasse()
    >>> c.article.section = 'Infos'
    >>> resolve_variable('article.section', c)
    'Infos'

    (Cet exemple suppose que VARIABLE_ATTRIBUTE_SEPARATOR vaut '.')
    """
    current = context
    bits = path.split(VARIABLE_ATTRIBUTE_SEPARATOR)
    while bits:
        try: # on essaie d'abord de chercher dans un dictionnaire
            current = current[bits[0]]
        except (TypeError, AttributeError, KeyError):
            try: # on essaie ensuite de chercher un attribut
                current = getattr(current, bits[0])
                if callable(current):
                    if getattr(current, 'alters_data', False):
                        current = ''
                    else:
                        try: # on essaie enfin l'appel de méthode (en supposant qu'aucun argument n'est requis)
                            current = current()
                        except SilentVariableFailure:
                            current = ''
                        except TypeError: # des arguments *étaient* requis
                            current = '' # appel invalide
            except (TypeError, AttributeError):
                try: # on essaie une recherche par indice
                    current = current[int(bits[0])]
                except (IndexError, ValueError, KeyError):
                    raise VariableDoesNotExist, "Impossible de trouver la clé [%s] dans %r" % (bits[0], current) # attribut manquant
        del bits[0]
    return current

def resolve_variable_with_filters(var_string, context):
    """
    L'argument var_string est une variable complète avec filtres facultatifs, par
    exemple : a.b.c|lower|date:"d/m/y".
    Cette fonction obtient la valeur de la variable dans le contexte spécifié, applique
    tous les filtres et renvoie l'objet ainsi obtenu.
    """
    var, filters = get_filters_from_token(var_string)
    try:
        obj = resolve_variable(var, context)
    except VariableDoesNotExist:
        obj = ''
    for name, arg in filters:
        obj = registered_filters[name][0](obj, arg)
    return obj

class Node:
    def render(self, context):
        "Renvoie le nœud sous forme de chaîne"
        pass

    def __iter__(self):
        yield self

    def get_nodes_by_type(self, nodetype):
        "Renvoie une liste de tous les nœuds du type spécifié (parmi ce nœud particulier et sa liste de nœuds)"
        nodes = []
        if isinstance(self, nodetype):
            nodes.append(self)
        if hasattr(self, 'nodelist'):
            nodes.extend(self.nodelist.get_nodes_by_type(nodetype))
        return nodes

class NodeList(list):
    def render(self, context):
        bits = []
        for node in self:
            if isinstance(node, Node):
                bits.append(node.render(context))
            else:
                bits.append(node)
        return ''.join(bits)

    def get_nodes_by_type(self, nodetype):
        "Renvoie une liste de tous les nœuds du type spécifié"
        nodes = []
        for node in self:
            nodes.extend(node.get_nodes_by_type(nodetype))
        return nodes

class TextNode(Node):
    def __init__(self, s):
        self.s = s

    def __repr__(self):
        return "<Nœud TextNode : '%s'>" % self.s[:25]

    def render(self, context):
        return self.s

class VariableNode(Node):
    def __init__(self, var_string):
        self.var_string = var_string

    def __repr__(self):
        return "<Nœud VariableNode : %s>" % self.var_string

    def render(self, context):
        output = resolve_variable_with_filters(self.var_string, context)
        # On vérifie le type pour ne pas exécuter str() sur un objet Unicode
        if not isinstance(output, basestring):
            output = str(output)
        elif isinstance(output, unicode):
            output = output.encode('utf-8')
        return output

def register_tag(token_command, callback_function):
    registered_tags[token_command] = callback_function

def unregister_tag(token_command):
    del registered_tags[token_command]

def register_filter(filter_name, callback_function, has_arg):
    registered_filters[filter_name] = (callback_function, has_arg)

def unregister_filter(filter_name):
    del registered_filters[filter_name]

import defaulttags
import defaultfilters
